/*-------------------------------------------------------------------------
 *
 * hashsearch.c
 *	  Postgres哈希表的搜索代码
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/hash/hashsearch.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/hash.h"
#include "access/relscan.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/predicate.h"
#include "utils/rel.h"

static bool fc__hash_readpage(IndexScanDesc fc_scan, Buffer *fc_bufP,
						   ScanDirection fc_dir);
static int	fc__hash_load_qualified_items(IndexScanDesc fc_scan, Page fc_page,
									   OffsetNumber fc_offnum, ScanDirection fc_dir);
static inline void fc__hash_saveitem(HashScanOpaque fc_so, int fc_itemIndex,
								  OffsetNumber fc_offnum, IndexTuple fc_itup);
static void fc__hash_readnext(IndexScanDesc fc_scan, Buffer *fc_bufp,
						   Page *fc_pagep, HashPageOpaque *fc_opaquep);

/*
 *	_hash_next() -- 获取扫描中的下一个项。
 *
 *		在进入时，so->currPos 描述当前页面，可能已被固定但未锁定，so->currPos.itemIndex 确定上一个返回的项。
 *
 *		在成功退出时，scan->xs_ctup.t_self 被设置为下一个堆元组的 TID。so->currPos 根据需要进行更新。
 *
 *		在失败退出（没有更多元组）时，我们返回 false，保持对存储桶页面的固定，但没有对溢出页面保持固定或锁定。
 */
bool _hash_next(IndexScanDesc fc_scan, ScanDirection fc_dir)
{
	Relation	fc_rel = fc_scan->indexRelation;
	HashScanOpaque fc_so = (HashScanOpaque) fc_scan->opaque;
	HashScanPosItem *fc_currItem;
	BlockNumber fc_blkno;
	Buffer		fc_buf;
	bool		fc_end_of_scan = false;

	/*
	 * 进入当前页面上的下一个元组；如果完成，则尝试根据扫描方向从下一个或上一个页面读取数据。在移动到下一个或上一个页面之前，请确保处理所有被删除的项。
	 */
	if (ScanDirectionIsForward(fc_dir))
	{
		if (++fc_so->currPos.itemIndex > fc_so->currPos.lastItem)
		{
			if (fc_so->numKilled > 0)
				_hash_kill_items(fc_scan);

			fc_blkno = fc_so->currPos.nextPage;
			if (BlockNumberIsValid(fc_blkno))
			{
				fc_buf = _hash_getbuf(fc_rel, fc_blkno, HASH_READ, LH_OVERFLOW_PAGE);
				TestForOldSnapshot(fc_scan->xs_snapshot, fc_rel, BufferGetPage(fc_buf));
				if (!fc__hash_readpage(fc_scan, &fc_buf, fc_dir))
					fc_end_of_scan = true;
			}
			else
				fc_end_of_scan = true;
		}
	}
	else
	{
		if (--fc_so->currPos.itemIndex < fc_so->currPos.firstItem)
		{
			if (fc_so->numKilled > 0)
				_hash_kill_items(fc_scan);

			fc_blkno = fc_so->currPos.prevPage;
			if (BlockNumberIsValid(fc_blkno))
			{
				fc_buf = _hash_getbuf(fc_rel, fc_blkno, HASH_READ,
								   LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
				TestForOldSnapshot(fc_scan->xs_snapshot, fc_rel, BufferGetPage(fc_buf));

				/*
				 * 我们始终在整个扫描操作中保持对存储桶页面的固定，
				 * 因此释放我们在此处获得的额外固定。
				 */
				if (fc_buf == fc_so->hashso_bucket_buf ||
					fc_buf == fc_so->hashso_split_bucket_buf)
					_hash_dropbuf(fc_rel, fc_buf);

				if (!fc__hash_readpage(fc_scan, &fc_buf, fc_dir))
					fc_end_of_scan = true;
			}
			else
				fc_end_of_scan = true;
		}
	}

	if (fc_end_of_scan)
	{
		_hash_dropscanbuf(fc_rel, fc_so);
		HashScanPosInvalidate(fc_so->currPos);
		return false;
	}

	/* 好的，itemIndex 表示要返回的内容 */
	fc_currItem = &fc_so->currPos.items[fc_so->currPos.itemIndex];
	fc_scan->xs_heaptid = fc_currItem->heapTid;

	return true;
}

/*
 * 进入存储桶中的下一个页面（如果有）。如果我们正在扫描在拆分操作期间填充的存储桶，则此函数在填充的存储桶的最后一个页面后进入正在拆分的存储桶。
 */
static void fc__hash_readnext(IndexScanDesc fc_scan,
			   Buffer *fc_bufp, Page *fc_pagep, HashPageOpaque *fc_opaquep)
{
	BlockNumber fc_blkno;
	Relation	fc_rel = fc_scan->indexRelation;
	HashScanOpaque fc_so = (HashScanOpaque) fc_scan->opaque;
	bool		fc_block_found = false;

	fc_blkno = (*fc_opaquep)->hasho_nextblkno;

	/*
	 * 保持对主存储桶页面的固定，直到扫描结束。请参考 _hash_first 中的注释以了解保持固定的原因。
	 */
	if (*fc_bufp == fc_so->hashso_bucket_buf || *fc_bufp == fc_so->hashso_split_bucket_buf)
		LockBuffer(*fc_bufp, BUFFER_LOCK_UNLOCK);
	else
		_hash_relbuf(fc_rel, *fc_bufp);

	*fc_bufp = InvalidBuffer;
	/* 在我们没有持有任何缓冲区锁时检查中断 */
	CHECK_FOR_INTERRUPTS();
	if (BlockNumberIsValid(fc_blkno))
	{
		*fc_bufp = _hash_getbuf(fc_rel, fc_blkno, HASH_READ, LH_OVERFLOW_PAGE);
		fc_block_found = true;
	}
	else if (fc_so->hashso_buc_populated && !fc_so->hashso_buc_split)
	{
		/*
		 * 存储桶的结束，如果在扫描开始时有拆分正在进行，则扫描正在拆分的存储桶。
		 */
		*fc_bufp = fc_so->hashso_split_bucket_buf;

		/*
		 * 正在拆分的存储桶的缓冲区必须是有效的，因为我们在扫描开始之前获取了对其的固定，并在扫描结束之前保持它。
		 */
		Assert(BufferIsValid(*fc_bufp));

		LockBuffer(*fc_bufp, BUFFER_LOCK_SHARE);
		PredicateLockPage(fc_rel, BufferGetBlockNumber(*fc_bufp), fc_scan->xs_snapshot);

		/*
		 * 将 hashso_buc_split 设置为 true 表示我们正在扫描
		 * 正在拆分的存储桶。
		 */
		fc_so->hashso_buc_split = true;

		fc_block_found = true;
	}

	if (fc_block_found)
	{
		*fc_pagep = BufferGetPage(*fc_bufp);
		TestForOldSnapshot(fc_scan->xs_snapshot, fc_rel, *fc_pagep);
		*fc_opaquep = HashPageGetOpaque(*fc_pagep);
	}
}

/*
 * 进入存储桶中的上一个页面（如果有）。如果当前扫描是在拆分操作期间开始的，则此函数在正在拆分的存储桶的第一个页面后进入填充的存储桶。
 */
static void fc__hash_readprev(IndexScanDesc fc_scan,
			   Buffer *fc_bufp, Page *fc_pagep, HashPageOpaque *fc_opaquep)
{
	BlockNumber fc_blkno;
	Relation	fc_rel = fc_scan->indexRelation;
	HashScanOpaque fc_so = (HashScanOpaque) fc_scan->opaque;
	bool		fc_haveprevblk;

	fc_blkno = (*fc_opaquep)->hasho_prevblkno;

	/*
	 * 保持对主存储桶页面的固定，直到扫描结束。请参考 _hash_first 中的注释以了解保持固定的原因。
	 */
	if (*fc_bufp == fc_so->hashso_bucket_buf || *fc_bufp == fc_so->hashso_split_bucket_buf)
	{
		LockBuffer(*fc_bufp, BUFFER_LOCK_UNLOCK);
		fc_haveprevblk = false;
	}
	else
	{
		_hash_relbuf(fc_rel, *fc_bufp);
		fc_haveprevblk = true;
	}

	*fc_bufp = InvalidBuffer;
	/* 在我们没有持有任何缓冲区锁时检查中断 */
	CHECK_FOR_INTERRUPTS();

	if (fc_haveprevblk)
	{
		Assert(BlockNumberIsValid(fc_blkno));
		*fc_bufp = _hash_getbuf(fc_rel, fc_blkno, HASH_READ,
							 LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
		*fc_pagep = BufferGetPage(*fc_bufp);
		TestForOldSnapshot(fc_scan->xs_snapshot, fc_rel, *fc_pagep);
		*fc_opaquep = HashPageGetOpaque(*fc_pagep);

		/*
		 * 我们始终在整个扫描操作中保持对存储桶页面的固定，
		 * 因此释放我们在此处获得的额外固定。
		 */
		if (*fc_bufp == fc_so->hashso_bucket_buf || *fc_bufp == fc_so->hashso_split_bucket_buf)
			_hash_dropbuf(fc_rel, *fc_bufp);
	}
	else if (fc_so->hashso_buc_populated && fc_so->hashso_buc_split)
	{
		/*
		 * 存储桶的结束，如果在扫描开始时有拆分正在进行，则扫描正在填充的存储桶。
		 */
		*fc_bufp = fc_so->hashso_bucket_buf;

		/*
		 * 正在填充的存储桶的缓冲区必须是有效的，因为我们在扫描开始之前获取了对其的固定，并在扫描结束之前保持它。
		 */
		Assert(BufferIsValid(*fc_bufp));

		LockBuffer(*fc_bufp, BUFFER_LOCK_SHARE);
		*fc_pagep = BufferGetPage(*fc_bufp);
		*fc_opaquep = HashPageGetOpaque(*fc_pagep);

		/* 移动到存储桶链的末端 */
		while (BlockNumberIsValid((*fc_opaquep)->hasho_nextblkno))
			fc__hash_readnext(fc_scan, fc_bufp, fc_pagep, fc_opaquep);

		/*
		 * 将 hashso_buc_split 设置为 false 表示我们正在扫描
		 * 正在填充的存储桶。
		 */
		fc_so->hashso_buc_split = false;
	}
}

/*
 *	_hash_first() -- 查找扫描中的第一个项目。
 *
 *		我们在索引中找到满足与扫描描述符相关的资格的第一个项目
 *		（或者，如果是向后扫描，则是最后一个项目）。
 *
 *		成功退出时，如果包含当前索引元组的页面是一个溢出页面，
 *		则释放pin和锁，而如果是一个桶页面，则它被固定但未被锁定，
 *		页面上匹配元组的数据已加载到so->currPos中，
 *		scan->xs_ctup.t_self被设置为当前元组的堆TID。
 *
 *		失败退出时（没有更多元组），我们返回false，桶页面上保持pin，
 *		但在溢出页面上没有保持pin或锁。
 */
bool _hash_first(IndexScanDesc fc_scan, ScanDirection fc_dir)
{
	Relation	fc_rel = fc_scan->indexRelation;
	HashScanOpaque fc_so = (HashScanOpaque) fc_scan->opaque;
	ScanKey		fc_cur;
	uint32		fc_hashkey;
	Bucket		fc_bucket;
	Buffer		fc_buf;
	Page		fc_page;
	HashPageOpaque fc_opaque;
	HashScanPosItem *fc_currItem;

	pgstat_count_index_scan(fc_rel);

	/*
	 * 我们不支持没有索引资格的哈希扫描，因为我们将不得不读取整个索引，
	 * 而不仅仅是一个桶。这会带来一系列问题，因为我们没有实际的方法
	 * 来锁定所有桶以防止拆分或合并。
	 */
	if (fc_scan->numberOfKeys < 1)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("hash indexes do not support whole-index scans")));

	/* 可能有多个索引资格，但我们只对第一个进行哈希 */
	fc_cur = &fc_scan->keyData[0];

	/* 我们仅支持单列哈希索引 */
	Assert(fc_cur->sk_attno == 1);
	/* 而且只有一个操作符策略 */
	Assert(fc_cur->sk_strategy == HTEqualStrategyNumber);

	/*
	 * 如果索引资格中的常量为NULL，则假设它无法匹配索引中的任何项目。
	 */
	if (fc_cur->sk_flags & SK_ISNULL)
		return false;

	/*
	 * 可以计算哈希键。我们希望在获取任何锁之前执行此操作，
	 * 以防用户定义的哈希函数非常慢。
	 *
	 * 如果扫描键操作符不是跨类型比较，我们可以使用
	 * 缓存的哈希函数；否则必须在目录中查找。
	 *
	 * 我们支持sk_subtype == InvalidOid表示操作类输入类型的约定；
	 * 这是一种简化ScanKeyInit()操作的方法。
	 */
	if (fc_cur->sk_subtype == fc_rel->rd_opcintype[0] ||
		fc_cur->sk_subtype == InvalidOid)
		fc_hashkey = _hash_datum2hashkey(fc_rel, fc_cur->sk_argument);
	else
		fc_hashkey = _hash_datum2hashkey_type(fc_rel, fc_cur->sk_argument,
										   fc_cur->sk_subtype);

	fc_so->hashso_sk_hash = fc_hashkey;

	fc_buf = _hash_getbucketbuf_from_hashkey(fc_rel, fc_hashkey, HASH_READ, NULL);
	PredicateLockPage(fc_rel, BufferGetBlockNumber(fc_buf), fc_scan->xs_snapshot);
	fc_page = BufferGetPage(fc_buf);
	TestForOldSnapshot(fc_scan->xs_snapshot, fc_rel, fc_page);
	fc_opaque = HashPageGetOpaque(fc_page);
	fc_bucket = fc_opaque->hasho_bucket;

	fc_so->hashso_bucket_buf = fc_buf;

	/*
	 * 如果正在进行桶拆分，则在扫描正在填充的桶时，
	 * 我们需要跳过从正在拆分的桶复制的元组。
	 * 我们还需要对正在拆分的桶保持pin，以确保由清理进程
	 * 完成的拆分工作不会在此扫描完成之前从中删除元组。
	 * 我们需要对正在填充的桶保持pin，以确保在此扫描完成之前
	 * 不会收缩该桶；否则，在向前和向后扫描期间，元组的顺序
	 * 将无法保持。这里，我们需要小心锁定顺序：首先，获取正在拆分的
	 * 桶上的锁；然后，释放该桶的锁但不释放pin；然后，
	 * 获取正在填充的桶上的锁，并再次验证桶拆分是否仍在进行中。
	 * 首先获取正在拆分的桶上的锁可确保清理进程等待此扫描完成。
	 */
	if (H_BUCKET_BEING_POPULATED(fc_opaque))
	{
		BlockNumber fc_old_blkno;
		Buffer		fc_old_buf;

		fc_old_blkno = _hash_get_oldblock_from_newbucket(fc_rel, fc_bucket);

		/*
		 * 释放新桶上的锁并在获取旧桶上的锁后重新获取它。
		 */
		LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);

		fc_old_buf = _hash_getbuf(fc_rel, fc_old_blkno, HASH_READ, LH_BUCKET_PAGE);
		TestForOldSnapshot(fc_scan->xs_snapshot, fc_rel, BufferGetPage(fc_old_buf));

		/*
		 * 记住拆分桶缓冲区，以便稍后用于扫描。
		 */
		fc_so->hashso_split_bucket_buf = fc_old_buf;
		LockBuffer(fc_old_buf, BUFFER_LOCK_UNLOCK);

		LockBuffer(fc_buf, BUFFER_LOCK_SHARE);
		fc_page = BufferGetPage(fc_buf);
		fc_opaque = HashPageGetOpaque(fc_page);
		Assert(fc_opaque->hasho_bucket == fc_bucket);

		if (H_BUCKET_BEING_POPULATED(fc_opaque))
			fc_so->hashso_buc_populated = true;
		else
		{
			_hash_dropbuf(fc_rel, fc_so->hashso_split_bucket_buf);
			fc_so->hashso_split_bucket_buf = InvalidBuffer;
		}
	}

	/* 如果请求向后扫描，移动到链的末端 */
	if (ScanDirectionIsBackward(fc_dir))
	{
		/*
		 * 后向扫描在拆分期间开始时需要从被拆分的桶的末尾开始。
		 */
		while (BlockNumberIsValid(fc_opaque->hasho_nextblkno) ||
			   (fc_so->hashso_buc_populated && !fc_so->hashso_buc_split))
			fc__hash_readnext(fc_scan, &fc_buf, &fc_page, &fc_opaque);
	}

	/* 记住我们锁定了哪个缓冲区（如果有的话） */
	Assert(BufferIsInvalid(fc_so->currPos.buf));
	fc_so->currPos.buf = fc_buf;

	/* 现在从一个页面找到所有满足条件的元组 */
	if (!fc__hash_readpage(fc_scan, &fc_buf, fc_dir))
		return false;

	/* 好的，itemIndex 表示要返回的内容 */
	fc_currItem = &fc_so->currPos.items[fc_so->currPos.itemIndex];
	fc_scan->xs_heaptid = fc_currItem->heapTid;

	/* 如果我们在这里，_hash_readpage 找到了有效的元组 */
	return true;
}

/*
 *	_hash_readpage() -- 从当前索引页面加载数据到 so->currPos
 *
 *	我们扫描当前索引页面中的所有项，若满足条件，则保存到
 *	so->currPos。如果在当前页面中没有找到匹配项，我们将根据方向
 *	移动到桶链中的下一个或上一个页面。
 *
 *	如果找到任何匹配项则返回 true，否则返回 false。
 */
static bool fc__hash_readpage(IndexScanDesc fc_scan, Buffer *fc_bufP, ScanDirection fc_dir)
{
	Relation	fc_rel = fc_scan->indexRelation;
	HashScanOpaque fc_so = (HashScanOpaque) fc_scan->opaque;
	Buffer		fc_buf;
	Page		fc_page;
	HashPageOpaque fc_opaque;
	OffsetNumber fc_offnum;
	uint16		fc_itemIndex;

	fc_buf = *fc_bufP;
	Assert(BufferIsValid(fc_buf));
	_hash_checkpage(fc_rel, fc_buf, LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
	fc_page = BufferGetPage(fc_buf);
	fc_opaque = HashPageGetOpaque(fc_page);

	fc_so->currPos.buf = fc_buf;
	fc_so->currPos.currPage = BufferGetBlockNumber(fc_buf);

	if (ScanDirectionIsForward(fc_dir))
	{
		BlockNumber fc_prev_blkno = InvalidBlockNumber;

		for (;;)
		{
			/* 新页面，通过二分搜索定位起始位置 */
			fc_offnum = _hash_binsearch(fc_page, fc_so->hashso_sk_hash);

			fc_itemIndex = fc__hash_load_qualified_items(fc_scan, fc_page, fc_offnum, fc_dir);

			if (fc_itemIndex != 0)
				break;

			/*
			 * 无法在当前页面中找到任何匹配的元组，移动到
			 * 下一个页面。在离开当前页面之前，处理任何
			 * 已删除的项。
			 */
			if (fc_so->numKilled > 0)
				_hash_kill_items(fc_scan);

			/*
			 * 如果这是一个主桶页面，hasho_prevblkno 不是一个真实
			 * 的块编号。
			 */
			if (fc_so->currPos.buf == fc_so->hashso_bucket_buf ||
				fc_so->currPos.buf == fc_so->hashso_split_bucket_buf)
				fc_prev_blkno = InvalidBlockNumber;
			else
				fc_prev_blkno = fc_opaque->hasho_prevblkno;

			fc__hash_readnext(fc_scan, &fc_buf, &fc_page, &fc_opaque);
			if (BufferIsValid(fc_buf))
			{
				fc_so->currPos.buf = fc_buf;
				fc_so->currPos.currPage = BufferGetBlockNumber(fc_buf);
			}
			else
			{
				/*
				 * 记住可滚动游标的下一个和上一个块编号，以了解起始位置并返回 false
				 * 表示未找到更多匹配的元组。此外，不要重置 currPage 或 lsn，
				 * 因为我们预计在此函数返回后会调用 _hash_kill_items 来处理旧页面。
				 */
				fc_so->currPos.prevPage = fc_prev_blkno;
				fc_so->currPos.nextPage = InvalidBlockNumber;
				fc_so->currPos.buf = fc_buf;
				return false;
			}
		}

		fc_so->currPos.firstItem = 0;
		fc_so->currPos.lastItem = fc_itemIndex - 1;
		fc_so->currPos.itemIndex = 0;
	}
	else
	{
		BlockNumber fc_next_blkno = InvalidBlockNumber;

		for (;;)
		{
			/* 新页面，通过二分搜索定位起始位置 */
			fc_offnum = _hash_binsearch_last(fc_page, fc_so->hashso_sk_hash);

			fc_itemIndex = fc__hash_load_qualified_items(fc_scan, fc_page, fc_offnum, fc_dir);

			if (fc_itemIndex != MaxIndexTuplesPerPage)
				break;

			/*
			 * 无法在当前页面中找到任何匹配的元组，移动到
			 * 上一个页面。在离开当前页面之前，处理任何
			 * 已删除的项。
			 */
			if (fc_so->numKilled > 0)
				_hash_kill_items(fc_scan);

			if (fc_so->currPos.buf == fc_so->hashso_bucket_buf ||
				fc_so->currPos.buf == fc_so->hashso_split_bucket_buf)
				fc_next_blkno = fc_opaque->hasho_nextblkno;

			fc__hash_readprev(fc_scan, &fc_buf, &fc_page, &fc_opaque);
			if (BufferIsValid(fc_buf))
			{
				fc_so->currPos.buf = fc_buf;
				fc_so->currPos.currPage = BufferGetBlockNumber(fc_buf);
			}
			else
			{
				/*
				 * 记住可滚动游标的下一个和上一个块编号，以了解起始位置并返回 false
				 * 表示未找到更多匹配的元组。此外，不要重置 currPage 或 lsn，
				 * 因为我们预计在此函数返回后会调用 _hash_kill_items 来处理旧页面。
				 */
				fc_so->currPos.prevPage = InvalidBlockNumber;
				fc_so->currPos.nextPage = fc_next_blkno;
				fc_so->currPos.buf = fc_buf;
				return false;
			}
		}

		fc_so->currPos.firstItem = fc_itemIndex;
		fc_so->currPos.lastItem = MaxIndexTuplesPerPage - 1;
		fc_so->currPos.itemIndex = MaxIndexTuplesPerPage - 1;
	}

	if (fc_so->currPos.buf == fc_so->hashso_bucket_buf ||
		fc_so->currPos.buf == fc_so->hashso_split_bucket_buf)
	{
		fc_so->currPos.prevPage = InvalidBlockNumber;
		fc_so->currPos.nextPage = fc_opaque->hasho_nextblkno;
		LockBuffer(fc_so->currPos.buf, BUFFER_LOCK_UNLOCK);
	}
	else
	{
		fc_so->currPos.prevPage = fc_opaque->hasho_prevblkno;
		fc_so->currPos.nextPage = fc_opaque->hasho_nextblkno;
		_hash_relbuf(fc_rel, fc_so->currPos.buf);
		fc_so->currPos.buf = InvalidBuffer;
	}

	Assert(fc_so->currPos.firstItem <= fc_so->currPos.lastItem);
	return true;
}

/*
 * 从当前索引页面加载所有合格的项到 so->currPos。
 * _hash_readpage 的助手函数。
 */
static int fc__hash_load_qualified_items(IndexScanDesc fc_scan, Page fc_page,
						   OffsetNumber fc_offnum, ScanDirection fc_dir)
{
	HashScanOpaque fc_so = (HashScanOpaque) fc_scan->opaque;
	IndexTuple	fc_itup;
	int			fc_itemIndex;
	OffsetNumber fc_maxoff;

	fc_maxoff = PageGetMaxOffsetNumber(fc_page);

	if (ScanDirectionIsForward(fc_dir))
	{
		/* 以升序加载 items[] */
		fc_itemIndex = 0;

		while (fc_offnum <= fc_maxoff)
		{
			Assert(fc_offnum >= FirstOffsetNumber);
			fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_offnum));

			/*
			 * 跳过因拆分操作而被移动的元组，针对在拆分进程中开始的扫描。
			 * 此外，跳过标记为已删除的元组。
			 */
			if ((fc_so->hashso_buc_populated && !fc_so->hashso_buc_split &&
				 (fc_itup->t_info & INDEX_MOVED_BY_SPLIT_MASK)) ||
				(fc_scan->ignore_killed_tuples &&
				 (ItemIdIsDead(PageGetItemId(fc_page, fc_offnum)))))
			{
				fc_offnum = OffsetNumberNext(fc_offnum);	/* 向前移动 */
				continue;
			}

			if (fc_so->hashso_sk_hash == _hash_get_indextuple_hashkey(fc_itup) &&
				_hash_checkqual(fc_scan, fc_itup))
			{
				/* 元组是合格的，因此记住它 */
				fc__hash_saveitem(fc_so, fc_itemIndex, fc_offnum, fc_itup);
				fc_itemIndex++;
			}
			else
			{
				/*
				 * 此页面中不存在更多匹配的元组。因此，退出 while
				 * 循环。
				 */
				break;
			}

			fc_offnum = OffsetNumberNext(fc_offnum);
		}

		Assert(fc_itemIndex <= MaxIndexTuplesPerPage);
		return fc_itemIndex;
	}
	else
	{
		/* 以降序加载 items[] */
		fc_itemIndex = MaxIndexTuplesPerPage;

		while (fc_offnum >= FirstOffsetNumber)
		{
			Assert(fc_offnum <= fc_maxoff);
			fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_offnum));

			/*
			 * 跳过因拆分操作而被移动的元组，针对在拆分进程中开始的扫描。
			 * 此外，跳过标记为已删除的元组。
			 */
			if ((fc_so->hashso_buc_populated && !fc_so->hashso_buc_split &&
				 (fc_itup->t_info & INDEX_MOVED_BY_SPLIT_MASK)) ||
				(fc_scan->ignore_killed_tuples &&
				 (ItemIdIsDead(PageGetItemId(fc_page, fc_offnum)))))
			{
				fc_offnum = OffsetNumberPrev(fc_offnum);	/* 向后移动 */
				continue;
			}

			if (fc_so->hashso_sk_hash == _hash_get_indextuple_hashkey(fc_itup) &&
				_hash_checkqual(fc_scan, fc_itup))
			{
				fc_itemIndex--;
				/* 元组是合格的，因此记住它 */
				fc__hash_saveitem(fc_so, fc_itemIndex, fc_offnum, fc_itup);
			}
			else
			{
				/*
				 * 此页面中不存在更多匹配的元组。因此，退出 while
				 * 循环。
				 */
				break;
			}

			fc_offnum = OffsetNumberPrev(fc_offnum);
		}

		Assert(fc_itemIndex >= 0);
		return fc_itemIndex;
	}
}

/* 将索引项保存到 so->currPos.items[itemIndex] */
static inline void fc__hash_saveitem(HashScanOpaque fc_so, int fc_itemIndex,
			   OffsetNumber fc_offnum, IndexTuple fc_itup)
{
	HashScanPosItem *fc_currItem = &fc_so->currPos.items[fc_itemIndex];

	fc_currItem->heapTid = fc_itup->t_tid;
	fc_currItem->indexOffset = fc_offnum;
}
